<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <link rel="stylesheet" href="./index.css">
  <script src="./index.js"></script>
  <style>
    .game-container {
      display: flex;
      width: 100%;
      /* max-height: 100vh; */
      max-width: 400px;
      background-color: rgb(252, 244, 233);
      padding: 16px;
      box-sizing: border-box;
      flex-direction: column;
      background: url('./test.jpg') no-repeat center / 100% 100%;
    }

    .game-board {
      position: relative;
      display: flex;
      flex-direction: column;
      width: 100%;
    }

    .game-board-title {
      font-size: 40px;
      font-weight: bold;
      color: rgb(168, 141, 102);
    }

    .game-board-subtitle {
      font-size: 14px;
      color: rgb(99, 76, 47);
    }

    .game-board-subtitle span {
      font-weight: bold;
    }

    .game-board-curscore,
    .game-board-maxscore {
      position: absolute;
      display: flex;
      flex-direction: column;
      align-items: center;
      background-color: rgb(185, 146, 99);
      color: #faf3e0;
      top: 0;
      padding: 4px 10px;
    }

    .game-board-maxscore {
      right: 0;
    }

    .game-board-curscore {
      right: 80px;
    }

    .game-board-label {
      font-size: 12px;
    }

    .game-grid {
      width: 100%;
      margin-top: 20px;
      background-color: #bbada0;
      position: relative;
      display: grid;
      padding: 10px;
      grid-row-gap: 10px;
      grid-column-gap: 10px;
      box-sizing: border-box;
    }

    .game-grid-gray {
      background-color: rgba(238, 228, 218, 0.35);
    }

    .game-grid-item {
      position: absolute;
      font-size: 20px;
      font-weight: bold;
      display: flex;
      justify-content: center;
      align-items: center;
      transition: all 0.25s ease-in-out;
    }

    .game-grid-item.init {
      animation-duration: 0.25s;
      animation-name: init;
      animation-iteration-count: unset;
    }

    .game-grid-item.merge {
      animation-delay: 0.25s;
      animation-duration: 0.25s;
      animation-name: merge;
      animation-iteration-count: unset;
    }

    .game-grid-pane {
      position: absolute;
      inset: 0;
      background: rgba(255, 255, 255, 0.6);
      display: flex;
      align-items: center;
      justify-content: center;
    }

    @keyframes init {
      0% {
        transform: scale(0.5);
      }

      100% {
        transform: scale(1);
      }
    }

    @keyframes merge {
      0% {
        transform: scale(0.8);
      }

      50% {
        transform: scale(1.2);
      }

      100% {
        transform: scale(1);
      }
    }
  </style>
</head>

<body>
  <div class="game-container">
    <div class="game-board">
      <div class="game-board-title">2048</div>
      <div class="game-board-subtitle">通过移动组合数字，目标达到<span>2048</span>!</div>

      <div class="game-board-curscore">
        <div class="game-board-label">当前分</div>
        <div class="game-board-value">0</div>
      </div>
      <div class="game-board-maxscore">
        <div class="game-board-label">最高分</div>
        <div class="game-board-value">0</div>
      </div>
    </div>
    <div class="game-grid"></div>
  </div>
</body>

<script type="module">
  import { Maths } from "https://gcore.jsdelivr.net/npm/@3r/tool/lib/maths.js";
  import { Randoms } from "https://gcore.jsdelivr.net/npm/@3r/tool/lib/randoms.js";

  // 宽高一致
  let gameGridDom = document.querySelector(".game-grid");

  let gameCurScoreDom = document.querySelector(".game-board-curscore .game-board-value");
  let gameMaxScoreDom = document.querySelector(".game-board-maxscore .game-board-value");

  let gameMaxScoreKey = "2048MaxScore";

  let gameCurScore = 0;
  let gameMaxScore = localStorage.getItem(gameMaxScoreKey) || 0;

  gameMaxScoreDom.textContent = gameMaxScore;

  gameGridDom.style.height = `${gameGridDom.clientWidth}px`;
  // 配置文件
  let config = {
    // 格子4×4
    gridSize: 4,
    gridItemColorList: [
      "rgb(238, 228, 218)",
      "rgb(237, 224, 200)",
      "rgb(242, 177, 121)",
      "rgb(245, 149, 99)",
      "rgb(246, 124, 95)",
      "rgb(246, 94, 59)",
      "rgb(237, 206, 115)",
      "rgb(236, 201, 97)",
      "rgb(238, 199, 80)",
      "rgb(239, 196, 65)",
      "rgb(239, 193, 46)",
      "rgb(255, 60, 61)",
      "rgb(255, 30, 32)",
    ],
  };

  // 格子元素字典
  let girdDomDict = {};
  // 格子地图
  let girdMap = {};
  // 格子元素地图
  let girdMapDom = {};
  // 游戏结束
  let isGameOver = false;
  // 输入回调参数
  let inputCallParam = {
    ArrowLeft: [-1, 0],
    ArrowRight: [1, 0],
    ArrowUp: [0, -1],
    ArrowDown: [0, 1],
  };

  // 初始化格子
  function initGrid() {
    for (let y = 0; y < config.gridSize; y++) {
      for (let x = 0; x < config.gridSize; x++) {
        let gridGray = document.createElement("div");
        girdDomDict[`${x},${y}`] = gridGray;
        gridGray.classList.add("game-grid-gray");
        gameGridDom.appendChild(gridGray);
      }
    }
    gameGridDom.style.gridTemplateColumns = `repeat(${config.gridSize},calc((100% - 30px) / ${config.gridSize}))`;
    gameGridDom.style.gridTemplateRows = `repeat(${config.gridSize},calc((100% - 30px) / ${config.gridSize}))`;
  }
  // 随机生成元素
  function randomGenerateElement() {
    // 判断游戏是否结束 逻辑限制此问题不存在
    // if (Object.values(girdMap).filter(item => item).length === config.gridSize * config.gridSize) {
    //     isGameOver = true;
    //     showGameOver('游戏结束 啊啦啦啦！/(ㄒoㄒ)/~~')
    //     return;
    // }

    let x = Randoms.getRandomInt(0, config.gridSize);
    let y = Randoms.getRandomInt(0, config.gridSize);
    // 如果格子存在元素 则重新随机
    if (girdMapDom[`${x},${y}`]) {
      return randomGenerateElement();
    }
    // 随机是 2 或者 4
    let value = Randoms.getRandomInt(0, 2) ? 2 : 4;
    let gridGray = girdDomDict[`${x},${y}`];
    let gridItem = document.createElement("div");
    gridItem.classList.add("game-grid-item");
    gridItem.classList.add("init");
    gridItem.style.left = gridGray.offsetLeft + "px";
    gridItem.style.top = gridGray.offsetTop + "px";
    gridItem.style.width = gridGray.offsetWidth + "px";
    gridItem.style.height = gridGray.offsetHeight + "px";

    gridItem.textContent = value;
    girdMap[`${x},${y}`] = value;

    elementColorTran(gridItem, value);

    girdMapDom[`${x},${y}`] = gridItem;
    gameGridDom.appendChild(gridItem);

    calculateCurScore();

    printMap();

    setTimeout(() => {
      gridItem.classList.remove("init");
    }, 250);

    // 检测是否还能合并
    checkGameOver();
  }
  let isSlideDirection = false;

  // 滑动方向
  function slideDirection(dx, dy) {
    if (isSlideDirection) return;
    isSlideDirection = true;
    // -1 , 0 向左
    //  1 , 0 向右
    //  0 , -1 向上  由于元素是从左上角为原点渲染的  所以这里就使用了 -1
    //  0 , 1 向下
    let numberOfTimes = 0; // 变更次数
    for (let i = 0; i < config.gridSize; i++) {
      let initJ = -1;
      for (let j = 0; j < config.gridSize - 1; j++) {
        for (let k = j + 1; k < config.gridSize; k++) {
          let curPos = "";
          let nexPos = "";

          if (dx == 1) {
            curPos = `${config.gridSize - 1 - j},${i}`;
            nexPos = `${config.gridSize - 1 - k},${i}`;
          }
          if (dx == -1) {
            curPos = `${j},${i}`;
            nexPos = `${k},${i}`;
          }
          if (dy == -1) {
            curPos = `${i},${j}`;
            nexPos = `${i},${k}`;
          }
          if (dy == 1) {
            curPos = `${i},${config.gridSize - 1 - j}`;
            nexPos = `${i},${config.gridSize - 1 - k}`;
          }

          const curVal = girdMap[curPos];
          const nexVal = girdMap[nexPos];

          if (nexVal) {
            if (!curVal) {
              // 位置为空 移动
              numberOfTimes++;
              moveElement(nexPos, curPos);
              j = initJ;
              break;
            } else if (curVal == nexVal) {
              // 位置值相等 合并
              numberOfTimes++;
              mergeElement(nexPos, curPos);
              initJ = j;
              break;
            } else if (curVal != nexVal) break; // 位置有值且不相同 跳过
          }
        }
      }
    }

    if (numberOfTimes) {
      setTimeout(() => {
        randomGenerateElement();
        isSlideDirection = false;
      }, 200);
    } else {
      isSlideDirection = false;
    }
  }
  // 移动元素
  function moveElement(origin, target) {
    let [ox, oy] = origin.split(",");
    let [tx, ty] = target.split(",");

    girdMap[target] = girdMap[origin];
    girdMap[origin] = undefined;

    girdMapDom[target] = girdMapDom[origin];
    girdMapDom[origin] = undefined;

    let gridGray = girdDomDict[`${tx},${ty}`];

    girdMapDom[target].style.left = gridGray.offsetLeft + "px";
    girdMapDom[target].style.top = gridGray.offsetTop + "px";
  }
  // 合并元素
  function mergeElement(origin, target) {
    /** @type {HTMLDivElement} */
    let originDom = girdMapDom[origin];
    /** @type {HTMLDivElement} */
    let targetDom = girdMapDom[target];

    let originVal = girdMap[origin];
    let targetVal = girdMap[target];
    let value = originVal + targetVal;

    moveElement(origin, target);

    girdMap[target] = value;

    elementColorTran(originDom, value);

    originDom.style.zIndex = 1;
    originDom.classList.add("merge");

    // 合成2048啦
    if (value == 2048) {
      isGameOver = true;
      showGameOver("👍（。＾▽＾）厉害了 哥们儿");
    }

    setTimeout(function () {
      originDom.textContent = value;
      originDom.style.zIndex = 0;
      targetDom.remove();
    }, 250);

    setTimeout(function () {
      originDom.classList.remove("merge");
    }, 500);
  }
  // 打印地图
  function printMap() {
    let str = "";
    for (let y = 0; y < config.gridSize; y++) {
      for (let x = 0; x < config.gridSize; x++) {
        str += `${girdMap[`${x},${y}`] || 0}  `;
      }
      str += "\n";
    }
  }
  // 元素颜色变换
  function elementColorTran(dom, val) {
    // 换算成二进制 获取0的个数
    let numberOfZeros = val
      .toString(2)
      .split("")
      .filter((item) => item == "0").length;
    // 颜色的索引
    let colorIndex = numberOfZeros - 1;
    // 获取颜色
    let color = config.gridItemColorList[colorIndex];
    // 给背景设置颜色
    dom.style.background = color;
  }
  // 计算得分
  function calculateCurScore() {
    gameCurScore = Maths.sum(Object.values(girdMap).filter((item) => item));
    gameCurScoreDom.textContent = gameCurScore;

    if (gameCurScore > gameMaxScore) {
      gameMaxScore = gameCurScore;
      gameMaxScoreDom.textContent = gameCurScore;
      localStorage.setItem(gameMaxScoreKey, gameCurScore);
    }
  }
  // 游戏胜利
  function showGameOver(conclusion) {
    let gameOverPane = document.querySelector(".game-grid-pane") || document.createElement("div");
    gameOverPane.classList.add("game-grid-pane");
    gameOverPane.textContent = conclusion;
    gameOverPane.style.zIndex = 2;
    gameGridDom.appendChild(gameOverPane);
  }
  // 检查死棋
  function checkGameOver() {
    let itemCount = Object.values(girdMap).filter((item) => item).length;
    // 当场上出现濒临死棋判断一下
    if (itemCount == config.gridSize * config.gridSize) {
      for (let x = 0; x < config.gridSize; x++) {
        for (let y = 0; y < config.gridSize; y++) {
          let topKey = `${x},${y - 1}`;
          let bottomKey = `${x},${y + 1}`;
          let leftKey = `${x - 1},${y}`;
          let rightKey = `${x + 1},${y}`;
          let selfKey = `${x},${y}`;

          let topVal = girdMap[topKey];
          let bottomVal = girdMap[bottomKey];
          let leftVal = girdMap[leftKey];
          let rightVal = girdMap[rightKey];
          let selfVal = girdMap[selfKey];

          if (topVal == selfVal) return;
          if (bottomVal == selfVal) return;
          if (leftVal == selfVal) return;
          if (rightVal == selfVal) return;
        }
      }

      isGameOver = true;
      showGameOver("游戏结束 啊啦啦啦！/(ㄒoㄒ)/~~");
    }
  }
  initGrid();
  // 生成两个 就掉了两次
  randomGenerateElement();
  randomGenerateElement();

  document.addEventListener("keydown", (ev) => {
    if (isGameOver) {
      return;
    }
    if (inputCallParam[ev.code]) {
      slideDirection(...inputCallParam[ev.code]);
    }
  });

  // 兼容触摸屏
  let touchStartX = null;
  let touchStartY = null;
  document.addEventListener("touchstart", (ev) => {
    let { pageX, pageY } = ev.changedTouches[0];
    touchStartX = pageX;
    touchStartY = pageY;
  });
  document.addEventListener("touchend", (ev) => {
    let { pageX, pageY } = ev.changedTouches[0];
    let offsetX = pageX - touchStartX;
    let offsetY = pageY - touchStartY;

    if (Math.abs(offsetX) > 20 || Math.abs(offsetY) > 20) {
      if (Math.abs(offsetX) > Math.abs(offsetY)) {
        slideDirection(Math.sign(offsetX), 0);
      } else {
        slideDirection(0, Math.sign(offsetY));
      }
    }
  });
</script>
</html>